/******************************************************************************
 *
 *  Copyright (C) 2014 Google, Inc.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at:
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 ******************************************************************************/

#define LOG_TAG "bt_l2cap_client"
#include "bt_target.h"
#if defined(L2CAP_CLIENT_INCLUDED) &&(L2CAP_CLIENT_INCLUDED == TRUE)
#include "stack/include/l2cap_client.h"

#include <assert.h>
#include <string.h>

#include "btcore/include/bdaddr.h"
#include "osi/include/allocator.h"
#include "osi/include/buffer.h"
#include "osi/include/list.h"
#include "osi/include/log.h"
#include "osi/include/osi.h"
#include "stack/include/l2c_api.h"

struct l2cap_client_t
{
    l2cap_client_callbacks_t callbacks;
    void *context;

    uint16_t local_channel_id;
    uint16_t remote_mtu;
    uint8_t configured_self;
    uint8_t configured_peer;
    uint8_t is_congested;
    list_t *outbound_fragments;
};

static void connect_completed_cb(uint16_t local_channel_id, uint16_t error_code);
static void config_request_cb(uint16_t local_channel_id, tL2CAP_CFG_INFO *requested_parameters);
static void config_completed_cb(uint16_t local_channel_id, tL2CAP_CFG_INFO *negotiated_parameters);
static void disconnect_request_cb(uint16_t local_channel_id, uint8_t ack_required);
static void disconnect_completed_cb(uint16_t local_channel_id, uint16_t error_code);
static void congestion_cb(uint16_t local_channel_id, uint8_t is_congested);
static void read_ready_cb(uint16_t local_channel_id, BT_HDR *packet);
static void write_completed_cb(uint16_t local_channel_id, uint16_t packets_completed);

static void fragment_packet(l2cap_client_t *client, buffer_t *packet);
static void dispatch_fragments(l2cap_client_t *client);
static l2cap_client_t *find(uint16_t local_channel_id);

// From the Bluetooth Core specification.
static const uint16_t L2CAP_MTU_DEFAULT = 672;
static const uint16_t L2CAP_MTU_MINIMUM = 48;

static const tL2CAP_APPL_INFO l2cap_callbacks =
{
    .pL2CA_ConnectCfm_Cb       = connect_completed_cb,
    .pL2CA_ConfigInd_Cb        = config_request_cb,
    .pL2CA_ConfigCfm_Cb        = config_completed_cb,
    .pL2CA_DisconnectInd_Cb    = disconnect_request_cb,
    .pL2CA_DisconnectCfm_Cb    = disconnect_completed_cb,
    .pL2CA_CongestionStatus_Cb = congestion_cb,
    .pL2CA_DataInd_Cb          = read_ready_cb,
    .pL2CA_TxComplete_Cb       = write_completed_cb,
};

static list_t *l2cap_clients;  // A list of l2cap_client_t. Container does not own objects.

buffer_t *l2cap_buffer_new(size_t size)
{
    buffer_t *buf = buffer_new(size + L2CAP_MIN_OFFSET);
    buffer_t *slice = NULL;

    if(buf)
    {
        slice = buffer_new_slice(buf, size);
    }

    buffer_free(buf);
    return slice;
}

l2cap_client_t *l2cap_client_new(const l2cap_client_callbacks_t *callbacks, void *context)
{
    assert(callbacks != NULL);
    assert(callbacks->connected != NULL);
    assert(callbacks->disconnected != NULL);
    assert(callbacks->read_ready != NULL);
    assert(callbacks->write_ready != NULL);

    if(!l2cap_clients)
    {
        l2cap_clients = list_new(NULL);

        if(!l2cap_clients)
        {
            LOG_ERROR(LOG_TAG, "%s unable to allocate space for L2CAP client list.", __func__);
            return NULL;
        }
    }

    l2cap_client_t *ret = (l2cap_client_t *)GKI_getbuf(sizeof(l2cap_client_t));
    ret->callbacks = *callbacks;
    ret->context = context;
    ret->remote_mtu = L2CAP_MTU_DEFAULT;
    ret->outbound_fragments = list_new(NULL);

    if(!ret)
    {
        LOG_ERROR(LOG_TAG, "%s unable to allocate outbound L2CAP fragment list.", __func__);
        goto error;
    }

    list_append(l2cap_clients, ret);
    return ret;
error:
    ;
    GKI_freebuf(ret);
    return NULL;
}

void l2cap_client_free(l2cap_client_t *client)
{
    if(!client)
    {
        return;
    }

    list_remove(l2cap_clients, client);
    l2cap_client_disconnect(client);
    list_free(client->outbound_fragments);
    GKI_freebuf(client);
}

uint8_t l2cap_client_connect(l2cap_client_t *client, const tls_bt_addr_t *remote_bdaddr, uint16_t psm)
{
    assert(client != NULL);
    assert(remote_bdaddr != NULL);
    assert(psm != 0);
    assert(!bdaddr_is_empty(remote_bdaddr));
    assert(client->local_channel_id == 0);
    assert(!client->configured_self);
    assert(!client->configured_peer);
    assert(!L2C_INVALID_PSM(psm));
    client->local_channel_id = L2CA_ConnectReq(psm, (uint8_t *)remote_bdaddr);

    if(!client->local_channel_id)
    {
        LOG_ERROR(LOG_TAG, "%s unable to create L2CAP connection.", __func__);
        return false;
    }

    L2CA_SetConnectionCallbacks(client->local_channel_id, &l2cap_callbacks);
    return true;
}

void l2cap_client_disconnect(l2cap_client_t *client)
{
    assert(client != NULL);

    if(client->local_channel_id && !L2CA_DisconnectReq(client->local_channel_id))
    {
        LOG_ERROR(LOG_TAG, "%s unable to send disconnect message for LCID 0x%04x.", __func__, client->local_channel_id);
    }

    client->local_channel_id = 0;
    client->remote_mtu = L2CAP_MTU_DEFAULT;
    client->configured_self = false;
    client->configured_peer = false;
    client->is_congested = false;

    for(const list_node_t *node = list_begin(client->outbound_fragments); node != list_end(client->outbound_fragments); node = list_next(node))
    {
        GKI_freebuf(list_node(node));
    }

    list_clear(client->outbound_fragments);
}

uint8_t l2cap_client_is_connected(const l2cap_client_t *client)
{
    assert(client != NULL);
    return client->local_channel_id != 0 && client->configured_self && client->configured_peer;
}

uint8_t l2cap_client_write(l2cap_client_t *client, buffer_t *packet)
{
    assert(client != NULL);
    assert(packet != NULL);
    assert(l2cap_client_is_connected(client));

    if(client->is_congested)
    {
        return false;
    }

    fragment_packet(client, packet);
    dispatch_fragments(client);
    return true;
}

static void connect_completed_cb(uint16_t local_channel_id, uint16_t error_code)
{
    assert(local_channel_id != 0);
    l2cap_client_t *client = find(local_channel_id);

    if(!client)
    {
        LOG_ERROR(LOG_TAG, "%s unable to find L2CAP client for LCID 0x%04x.", __func__, local_channel_id);
        return;
    }

    if(error_code != L2CAP_CONN_OK)
    {
        LOG_ERROR(LOG_TAG, "%s error connecting L2CAP channel: %d.", __func__, error_code);
        client->callbacks.disconnected(client, client->context);
        return;
    }

    // Use default L2CAP parameters.
    tL2CAP_CFG_INFO desired_parameters;
    wm_memset(&desired_parameters, 0, sizeof(desired_parameters));

    if(!L2CA_ConfigReq(local_channel_id, &desired_parameters))
    {
        LOG_ERROR(LOG_TAG, "%s error sending L2CAP config parameters.", __func__);
        client->callbacks.disconnected(client, client->context);
    }
}

static void config_request_cb(uint16_t local_channel_id, tL2CAP_CFG_INFO *requested_parameters)
{
    tL2CAP_CFG_INFO response;
    l2cap_client_t *client = find(local_channel_id);

    if(!client)
    {
        LOG_ERROR(LOG_TAG, "%s unable to find L2CAP client matching LCID 0x%04x.", __func__, local_channel_id);
        return;
    }

    wm_memset(&response, 0, sizeof(response));
    response.result = L2CAP_CFG_OK;

    if(requested_parameters->mtu_present)
    {
        // Make sure the peer chose an MTU at least as large as the minimum L2CAP MTU defined
        // by the Bluetooth Core spec.
        if(requested_parameters->mtu < L2CAP_MTU_MINIMUM)
        {
            response.mtu = L2CAP_MTU_MINIMUM;
            response.mtu_present = true;
            response.result = L2CAP_CFG_UNACCEPTABLE_PARAMS;
        }
        else
        {
            client->remote_mtu = requested_parameters->mtu;
        }
    }

    if(requested_parameters->fcr_present)
    {
        if(requested_parameters->fcr.mode != L2CAP_FCR_BASIC_MODE)
        {
            response.fcr_present = true;
            response.fcr = requested_parameters->fcr;
            response.fcr.mode = L2CAP_FCR_BASIC_MODE;
            response.result = L2CAP_CFG_UNACCEPTABLE_PARAMS;
        }
    }

    if(!L2CA_ConfigRsp(local_channel_id, &response))
    {
        LOG_ERROR(LOG_TAG, "%s unable to send config response for LCID 0x%04x.", __func__, local_channel_id);
        l2cap_client_disconnect(client);
        return;
    }

    // If we've configured both endpoints, let the listener know we've connected.
    client->configured_peer = true;

    if(l2cap_client_is_connected(client))
    {
        client->callbacks.connected(client, client->context);
    }
}

static void config_completed_cb(uint16_t local_channel_id, tL2CAP_CFG_INFO *negotiated_parameters)
{
    l2cap_client_t *client = find(local_channel_id);

    if(!client)
    {
        LOG_ERROR(LOG_TAG, "%s unable to find L2CAP client matching LCID 0x%04x.", __func__, local_channel_id);
        return;
    }

    switch(negotiated_parameters->result)
    {
        // We'll get another configuration response later.
        case L2CAP_CFG_PENDING:
            break;

        case L2CAP_CFG_UNACCEPTABLE_PARAMS:
            // TODO: see if we can renegotiate parameters instead of dropping the connection.
            LOG_WARN(LOG_TAG, "%s dropping L2CAP connection due to unacceptable config parameters.", __func__);
            l2cap_client_disconnect(client);
            break;

        case L2CAP_CFG_OK:
            // If we've configured both endpoints, let the listener know we've connected.
            client->configured_self = true;

            if(l2cap_client_is_connected(client))
            {
                client->callbacks.connected(client, client->context);
            }

            break;

        // Failure, no further parameter negotiation possible.
        default:
            LOG_WARN(LOG_TAG, "%s L2CAP parameter negotiation failed with error code %d.", __func__, negotiated_parameters->result);
            l2cap_client_disconnect(client);
            break;
    }
}

static void disconnect_request_cb(uint16_t local_channel_id, uint8_t ack_required)
{
    l2cap_client_t *client = find(local_channel_id);

    if(!client)
    {
        LOG_ERROR(LOG_TAG, "%s unable to find L2CAP client with LCID 0x%04x.", __func__, local_channel_id);
        return;
    }

    if(ack_required)
    {
        L2CA_DisconnectRsp(local_channel_id);
    }

    // We already sent a disconnect response so this LCID is now invalid.
    client->local_channel_id = 0;
    l2cap_client_disconnect(client);
    client->callbacks.disconnected(client, client->context);
}

static void disconnect_completed_cb(uint16_t local_channel_id, UNUSED_ATTR uint16_t error_code)
{
    assert(local_channel_id != 0);
    l2cap_client_t *client = find(local_channel_id);

    if(!client)
    {
        LOG_ERROR(LOG_TAG, "%s unable to find L2CAP client with LCID 0x%04x.", __func__, local_channel_id);
        return;
    }

    client->local_channel_id = 0;
    l2cap_client_disconnect(client);
    client->callbacks.disconnected(client, client->context);
}

static void congestion_cb(uint16_t local_channel_id, uint8_t is_congested)
{
    assert(local_channel_id != 0);
    l2cap_client_t *client = find(local_channel_id);

    if(!client)
    {
        LOG_ERROR(LOG_TAG, "%s unable to find L2CAP client matching LCID 0x%04x.", __func__, local_channel_id);
        return;
    }

    client->is_congested = is_congested;

    if(!is_congested)
    {
        // If we just decongested, dispatch whatever we have left over in our queue.
        // Once that's done, if we're still decongested, notify the listener so it
        // can start writing again.
        dispatch_fragments(client);

        if(!client->is_congested)
        {
            client->callbacks.write_ready(client, client->context);
        }
    }
}

static void read_ready_cb(uint16_t local_channel_id, BT_HDR *packet)
{
    assert(local_channel_id != 0);
    l2cap_client_t *client = find(local_channel_id);

    if(!client)
    {
        LOG_ERROR(LOG_TAG, "%s unable to find L2CAP client matching LCID 0x%04x.", __func__, local_channel_id);
        return;
    }

    // TODO(sharvil): eliminate copy from BT_HDR.
    buffer_t *buffer = buffer_new(packet->len);
    wm_memcpy(buffer_ptr(buffer), packet->data + packet->offset, packet->len);
    GKI_freebuf(packet);
    client->callbacks.read_ready(client, buffer, client->context);
    buffer_free(buffer);
}

static void write_completed_cb(UNUSED_ATTR uint16_t local_channel_id, UNUSED_ATTR uint16_t packets_completed)
{
    // Do nothing. We update congestion state based on the congestion callback
    // and we've already removed items from outbound_fragments list so we don't
    // really care how many packets were successfully dispatched.
}

static void fragment_packet(l2cap_client_t *client, buffer_t *packet)
{
    assert(client != NULL);
    assert(packet != NULL);
    // TODO(sharvil): eliminate copy into BT_HDR.
    BT_HDR *bt_packet = GKI_getbuf(buffer_length(packet) + L2CAP_MIN_OFFSET);
    bt_packet->offset = L2CAP_MIN_OFFSET;
    bt_packet->len = buffer_length(packet);
    wm_memcpy(bt_packet->data + bt_packet->offset, buffer_ptr(packet), buffer_length(packet));

    for(;;)
    {
        if(bt_packet->len <= client->remote_mtu)
        {
            if(bt_packet->len > 0)
            {
                list_append(client->outbound_fragments, bt_packet);
            }
            else
            {
                GKI_freebuf(bt_packet);
            }

            break;
        }

        BT_HDR *fragment = GKI_getbuf(client->remote_mtu + L2CAP_MIN_OFFSET);
        fragment->offset = L2CAP_MIN_OFFSET;
        fragment->len = client->remote_mtu;
        wm_memcpy(fragment->data + fragment->offset, bt_packet->data + bt_packet->offset, client->remote_mtu);
        list_append(client->outbound_fragments, fragment);
        bt_packet->offset += client->remote_mtu;
        bt_packet->len -= client->remote_mtu;
    }
}

static void dispatch_fragments(l2cap_client_t *client)
{
    assert(client != NULL);
    assert(!client->is_congested);

    while(!list_is_empty(client->outbound_fragments))
    {
        BT_HDR *packet = (BT_HDR *)list_front(client->outbound_fragments);
        list_remove(client->outbound_fragments, packet);

        switch(L2CA_DataWrite(client->local_channel_id, packet))
        {
            case L2CAP_DW_CONGESTED:
                client->is_congested = true;
                return;

            case L2CAP_DW_FAILED:
                LOG_ERROR(LOG_TAG, "%s error writing data to L2CAP connection LCID 0x%04x; disconnecting.", __func__, client->local_channel_id);
                l2cap_client_disconnect(client);
                return;

            case L2CAP_DW_SUCCESS:
                break;
        }
    }
}

static l2cap_client_t *find(uint16_t local_channel_id)
{
    assert(local_channel_id != 0);

    for(const list_node_t *node = list_begin(l2cap_clients); node != list_end(l2cap_clients); node = list_next(node))
    {
        l2cap_client_t *client = (l2cap_client_t *)list_node(node);

        if(client->local_channel_id == local_channel_id)
        {
            return client;
        }
    }

    return NULL;
}
#endif
